package org.gluedom.marshall;

import static com.google.common.base.Preconditions.checkArgument;

import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.gluedom.data.Attribute;
import org.gluedom.data.AttributeValue;
import org.gluedom.data.Concept;
import org.gluedom.data.ConceptRelation;
import org.gluedom.data.Identifier;
import org.gluedom.data.Immutable;
import org.gluedom.data.Property;

import com.google.common.collect.ImmutableList;

class BinaryMarshaller implements Marshaller {
	private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
	private static final byte ID_STRING = 1;
	private static final byte ID_BYTE = 2;
	private static final byte ID_SHORT = 3;
	private static final byte ID_INT = 4;
	private static final byte ID_LONG = 5;
	private static final byte ID_FLOAT = 6;
	private static final byte ID_DOUBLE = 7;
	private static final byte ID_BOOLEAN = 8;
	private static final byte ID_CHAR = 9;
	private static final byte ID_PROPERTY = 10;
	private static final byte ID_IDENTIFIER = 11;
	private static final byte ID_ATTRIBUTE = 12;
	private static final byte ID_RELATION = 13;
	private static final byte ID_CONCEPT = 14;
	private static final byte ID_ATTRIBUTEVALUE = 15;

	private static enum BinaryHelper {
		STRING {
			@Override
			public int estimateSize(Object obj) {
				return ((String) obj).length() * 2 + 5;
			}

			@Override
			public byte idByte() {
				return ID_STRING;
			}

			@Override
			public void marshall(ByteBuffer bytes, Object obj) {
				super.marshall(bytes, obj);
				String typed = (String) obj;
				byte[] b = typed.getBytes(DEFAULT_CHARSET);
				bytes.putInt(b.length);
				bytes.put(b);
			}

			@Override
			public Object unmarshall(ByteBuffer bytes) {
				super.unmarshall(bytes);
				int length = bytes.getInt();
				String result = new String(bytes.array(), bytes.position(),
						length, DEFAULT_CHARSET);
				bytes.position(bytes.position() + length);
				return result;
			}
		},
		BYTE {
			@Override
			public int estimateSize(Object obj) {
				return 2;
			}

			@Override
			public byte idByte() {
				return ID_BYTE;
			}

			@Override
			public void marshall(ByteBuffer bytes, Object obj) {
				super.marshall(bytes, obj);
				bytes.put((Byte) obj);
			}

			@Override
			public Object unmarshall(ByteBuffer bytes) {
				super.unmarshall(bytes);
				return bytes.get();
			}
		},
		SHORT {
			@Override
			public int estimateSize(Object obj) {
				return 3;
			}

			@Override
			public byte idByte() {
				return ID_SHORT;
			}

			@Override
			public void marshall(ByteBuffer bytes, Object obj) {
				super.marshall(bytes, obj);
				bytes.putShort((Short) obj);
			}

			@Override
			public Object unmarshall(ByteBuffer bytes) {
				super.unmarshall(bytes);
				return bytes.getShort();
			}
		},
		INT {
			@Override
			public int estimateSize(Object obj) {
				return 5;
			}

			@Override
			public byte idByte() {
				return ID_INT;
			}

			@Override
			public void marshall(ByteBuffer bytes, Object obj) {
				super.marshall(bytes, obj);
				bytes.putInt((Integer) obj);
			}

			@Override
			public Object unmarshall(ByteBuffer bytes) {
				super.unmarshall(bytes);
				return bytes.getInt();
			}
		},
		LONG {
			@Override
			public int estimateSize(Object obj) {
				return 9;
			}

			@Override
			public byte idByte() {
				return ID_LONG;
			}

			@Override
			public void marshall(ByteBuffer bytes, Object obj) {
				super.marshall(bytes, obj);
				bytes.putLong((Long) obj);
			}

			@Override
			public Object unmarshall(ByteBuffer bytes) {
				super.unmarshall(bytes);
				return bytes.getLong();
			}
		},
		FLOAT {
			@Override
			public int estimateSize(Object obj) {
				return 5;
			}

			@Override
			public byte idByte() {
				return ID_FLOAT;
			}

			@Override
			public void marshall(ByteBuffer bytes, Object obj) {
				super.marshall(bytes, obj);
				bytes.putFloat((Float) obj);
			}

			@Override
			public Object unmarshall(ByteBuffer bytes) {
				super.unmarshall(bytes);
				return bytes.getFloat();
			}
		},
		DOUBLE {
			@Override
			public int estimateSize(Object obj) {
				return 9;
			}

			@Override
			public byte idByte() {
				return ID_DOUBLE;
			}

			@Override
			public void marshall(ByteBuffer bytes, Object obj) {
				super.marshall(bytes, obj);
				bytes.putDouble((Double) obj);
			}

			@Override
			public Object unmarshall(ByteBuffer bytes) {
				super.unmarshall(bytes);
				return bytes.getDouble();
			}
		},
		BOOLEAN {
			@Override
			public int estimateSize(Object obj) {
				return 2;
			}

			@Override
			public byte idByte() {
				return ID_BOOLEAN;
			}

			@Override
			public void marshall(ByteBuffer bytes, Object obj) {
				super.marshall(bytes, obj);
				bytes.put(((Boolean) obj).booleanValue() ? (byte) 1 : (byte) 0);
			}

			@Override
			public Object unmarshall(ByteBuffer bytes) {
				super.unmarshall(bytes);
				return (bytes.get() == 1 ? true : false);
			}
		},
		CHAR {
			@Override
			public int estimateSize(Object obj) {
				return 3;
			}

			@Override
			public byte idByte() {
				return ID_CHAR;
			}

			@Override
			public void marshall(ByteBuffer bytes, Object obj) {
				super.marshall(bytes, obj);
				bytes.putChar((Character) obj);
			}

			@Override
			public Object unmarshall(ByteBuffer bytes) {
				super.unmarshall(bytes);
				return bytes.getChar();
			}
		},
		PROPERTY {
			@Override
			public int estimateSize(Object obj) {
				Property typed = (Property) obj;
				return STRING.estimateSize(typed.getName())
						+ STRING.estimateSize(typed.getValue()) + 1;
			}

			@Override
			public byte idByte() {
				return ID_PROPERTY;
			}

			@Override
			public void marshall(ByteBuffer bytes, Object obj) {
				super.marshall(bytes, obj);
				Property typed = (Property) obj;
				STRING.marshall(bytes, typed.getName());
				STRING.marshall(bytes, typed.getValue());
			}

			@Override
			public Object unmarshall(ByteBuffer bytes) {
				super.unmarshall(bytes);
				return Property.of((String) STRING.unmarshall(bytes),
						(String) STRING.unmarshall(bytes));
			}
		},
		IDENTIFIER {
			@Override
			public int estimateSize(Object obj) {
				Identifier typed = (Identifier) obj;
				return STRING.estimateSize(Identifier.marshall(typed) + 1);
			}

			@Override
			public byte idByte() {
				return ID_IDENTIFIER;
			}

			@Override
			public void marshall(ByteBuffer bytes, Object obj) {
				super.marshall(bytes, obj);
				Identifier typed = (Identifier) obj;
				STRING.marshall(bytes, Identifier.marshall(typed));
			}

			@Override
			public Object unmarshall(ByteBuffer bytes) {
				super.unmarshall(bytes);
				return Identifier.unmarshall((String) STRING.unmarshall(bytes));
			}
		},
		ATTRIBUTE {
			@Override
			public int estimateSize(Object obj) {
				int result = 0;
				Attribute typed = (Attribute) obj;
				result += IDENTIFIER.estimateSize(typed.getIdentifier());
				result += STRING.estimateSize(typed.getType());
				result += LIST.estimateSize(typed.getProperties());
				return result;
			}

			@Override
			public byte idByte() {
				return ID_ATTRIBUTE;
			}

			@Override
			public void marshall(ByteBuffer bytes, Object obj) {
				super.marshall(bytes, obj);
				Attribute typed = (Attribute) obj;
				IDENTIFIER.marshall(bytes, typed.getIdentifier());
				STRING.marshall(bytes, typed.getType());
				LIST.marshall(bytes, typed.getProperties());
			}

			@Override
			public Object unmarshall(ByteBuffer bytes) {
				super.unmarshall(bytes);
				Attribute.Builder builder = new Attribute.Builder(
						(Identifier) IDENTIFIER.unmarshall(bytes),
						(String) STRING.unmarshall(bytes));
				while (bytes.position() < bytes.limit()
						&& bytes.get(bytes.position()) == PROPERTY.idByte())
					builder.property((Property) PROPERTY.unmarshall(bytes));
				return builder.build();
			}

		},
		RELATION {
			@Override
			public int estimateSize(Object obj) {
				int result = 0;
				ConceptRelation typed = (ConceptRelation) obj;
				result += IDENTIFIER.estimateSize(typed.getIdentifier());
				result += IDENTIFIER.estimateSize(typed.getOwnerId());
				result += IDENTIFIER.estimateSize(typed.getInConcept());
				result += IDENTIFIER.estimateSize(typed.getOutConcept());
				result += LIST.estimateSize(typed.getProperties());
				return result;
			}

			@Override
			public byte idByte() {
				return ID_RELATION;
			}

			@Override
			public void marshall(ByteBuffer bytes, Object obj) {
				super.marshall(bytes, obj);
				ConceptRelation typed = (ConceptRelation) obj;
				IDENTIFIER.marshall(bytes, typed.getIdentifier());
				IDENTIFIER.marshall(bytes, typed.getOwnerId());
				IDENTIFIER.marshall(bytes, typed.getInConcept());
				IDENTIFIER.marshall(bytes, typed.getOutConcept());
				LIST.marshall(bytes, typed.getProperties());
			}

			@Override
			public Object unmarshall(ByteBuffer bytes) {
				super.unmarshall(bytes);
				ConceptRelation.Builder builder = new ConceptRelation.Builder(
						(Identifier) IDENTIFIER.unmarshall(bytes),
						(Identifier) IDENTIFIER.unmarshall(bytes),
						(Identifier) IDENTIFIER.unmarshall(bytes),
						(Identifier) IDENTIFIER.unmarshall(bytes));
				while (bytes.position() < bytes.limit()
						&& bytes.get(bytes.position()) == PROPERTY.idByte())
					builder.property((Property) PROPERTY.unmarshall(bytes));
				return builder.build();
			}
		},
		CONCEPT {
			@Override
			public int estimateSize(Object obj) {
				int result = 0;
				Concept typed = (Concept) obj;
				result += IDENTIFIER.estimateSize(typed.getIdentifier());
				result += IDENTIFIER.estimateSize(typed.getOwnerId());
				result += LIST.estimateSize(typed.getAttributes());
				result += LIST.estimateSize(typed.getRelations());
				result += LIST.estimateSize(typed.getProperties());
				return result;
			}

			@Override
			public byte idByte() {
				return ID_CONCEPT;
			}

			@Override
			public void marshall(ByteBuffer bytes, Object obj) {
				super.marshall(bytes, obj);
				Concept typed = (Concept) obj;
				IDENTIFIER.marshall(bytes, typed.getIdentifier());
				IDENTIFIER.marshall(bytes, typed.getOwnerId());
				LIST.marshall(bytes, typed.getAttributes());
				LIST.marshall(bytes, typed.getRelations());
				LIST.marshall(bytes, typed.getProperties());
			}

			@Override
			public Object unmarshall(ByteBuffer bytes) {
				super.unmarshall(bytes);
				Concept.Builder builder = new Concept.Builder(
						(Identifier) IDENTIFIER.unmarshall(bytes),
						(Identifier) IDENTIFIER.unmarshall(bytes));
				while (bytes.position() < bytes.limit()
						&& bytes.get(bytes.position()) == ATTRIBUTE.idByte())
					builder.attribute((Attribute) ATTRIBUTE.unmarshall(bytes));
				while (bytes.position() < bytes.limit()
						&& bytes.get(bytes.position()) == RELATION.idByte())
					builder.relation((ConceptRelation) RELATION
							.unmarshall(bytes));
				while (bytes.position() < bytes.limit()
						&& bytes.get(bytes.position()) == PROPERTY.idByte())
					builder.property((Property) PROPERTY.unmarshall(bytes));
				return builder.build();
			}
		},
		ATTRIBUTEVALUE {
			@Override
			public int estimateSize(Object obj) {
				int result = 0;
				AttributeValue typed = (AttributeValue) obj;
				result += IDENTIFIER.estimateSize(typed.getOwnerId());
				result += IDENTIFIER.estimateSize(typed.getConceptId());
				result += IDENTIFIER.estimateSize(typed.getAttributeId());
				result += BinaryHelper.fromObject(typed.getValue())
						.estimateSize(typed.getValue());
				result += LIST.estimateSize(typed.getProperties());
				return result;
			}

			@Override
			public byte idByte() {
				return ID_ATTRIBUTEVALUE;
			}

			@Override
			public void marshall(ByteBuffer bytes, Object obj) {
				super.marshall(bytes, obj);
				AttributeValue typed = (AttributeValue) obj;
				IDENTIFIER.marshall(bytes, typed.getOwnerId());
				IDENTIFIER.marshall(bytes, typed.getConceptId());
				IDENTIFIER.marshall(bytes, typed.getAttributeId());
				BinaryHelper.fromObject(typed.getValue()).marshall(bytes,
						typed.getValue());
				LIST.marshall(bytes, typed.getProperties());
			}

			@Override
			public Object unmarshall(ByteBuffer bytes) {
				super.unmarshall(bytes);
				AttributeValue.Builder builder = new AttributeValue.Builder(
						(Identifier) IDENTIFIER.unmarshall(bytes),
						(Identifier) IDENTIFIER.unmarshall(bytes),
						(Identifier) IDENTIFIER.unmarshall(bytes), BinaryHelper
								.fromByte(bytes.get(bytes.position()))
								.unmarshall(bytes));
				while (bytes.position() < bytes.limit()
						&& bytes.get(bytes.position()) == PROPERTY.idByte())
					builder.property((Property) PROPERTY.unmarshall(bytes));
				return builder.build();
			}
		},
		LIST {
			@Override
			public int estimateSize(Object obj) {
				@SuppressWarnings("unchecked")
				Iterable<? extends Object> list = (Iterable<? extends Object>) obj;
				int result = 0;
				for (Object o : list)
					result += BinaryHelper.fromObject(o).estimateSize(o);
				return result;
			}

			@Override
			public byte idByte() {
				return -1;
			}

			@Override
			public void marshall(ByteBuffer bytes, Object obj) {
				@SuppressWarnings("unchecked")
				Iterable<? extends Object> list = (Iterable<? extends Object>) obj;
				for (Object o : list)
					BinaryHelper.fromObject(o).marshall(bytes, o);
			}

			@Override
			public Object unmarshall(ByteBuffer bytes) {
				throw new UnsupportedOperationException();
			}
		};

		public abstract byte idByte();

		public abstract int estimateSize(Object obj);

		public void marshall(ByteBuffer bytes, Object obj) {
			bytes.put(idByte());
		}

		public Object unmarshall(ByteBuffer bytes) {
			checkArgument(bytes.get() == idByte());
			return null;
		}

		public static BinaryHelper fromByte(byte id) {
			for (BinaryHelper bh : BinaryHelper.values())
				if (bh.idByte() == id)
					return bh;
			throw new IllegalArgumentException(
					"argument is not supported by this marshaller");
		}

		public static BinaryHelper fromObject(Object obj) {
			if (obj instanceof String)
				return STRING;
			if (obj instanceof Byte)
				return BYTE;
			if (obj instanceof Short)
				return SHORT;
			if (obj instanceof Integer)
				return INT;
			if (obj instanceof Long)
				return LONG;
			if (obj instanceof Float)
				return FLOAT;
			if (obj instanceof Double)
				return DOUBLE;
			if (obj instanceof Boolean)
				return BOOLEAN;
			if (obj instanceof Character)
				return CHAR;
			if (obj instanceof Property)
				return PROPERTY;
			if (obj instanceof Identifier)
				return IDENTIFIER;
			if (obj instanceof Attribute)
				return ATTRIBUTE;
			if (obj instanceof ConceptRelation)
				return RELATION;
			if (obj instanceof Concept)
				return CONCEPT;
			if (obj instanceof AttributeValue)
				return ATTRIBUTEVALUE;
			throw new IllegalArgumentException(
					"argument is not supported by this marshaller");
		}
	}

	@Override
	public byte[] marshallList(List<? extends Immutable> immutables) {
		int size = 0;
		for (Immutable immutable : immutables)
			size += BinaryHelper.fromObject(immutable).estimateSize(immutable);
		ByteBuffer bytes = ByteBuffer.allocate(size);
		for (Immutable immutable : immutables)
			BinaryHelper.fromObject(immutable).marshall(bytes, immutable);
		return Arrays.copyOf(bytes.array(), bytes.position());
	}

	@Override
	public <T extends Immutable> List<T> unmarshallList(byte[] bytes) {
		if (bytes.length == 0)
			return ImmutableList.of();
		ByteBuffer buffer = ByteBuffer.wrap(bytes);
		List<Immutable> list = new ArrayList<Immutable>();
		while (buffer.position() < buffer.limit())
			list.add((Immutable) BinaryHelper.fromByte(
					buffer.get(buffer.position())).unmarshall(buffer));
		@SuppressWarnings("unchecked")
		List<T> result = (List<T>) ImmutableList.copyOf(list);
		return result;
	};

	public byte[] marshall(Immutable immutable) {
		BinaryHelper helper = BinaryHelper.fromObject(immutable);
		int size = helper.estimateSize(immutable);
		ByteBuffer result = ByteBuffer.allocate(size);
		helper.marshall(result, immutable);
		return Arrays.copyOf(result.array(), result.position());
	}

	public <T extends Immutable> T unmarshall(byte[] bytes) {
		if (bytes.length == 0)
			return null;
		ByteBuffer buffer = ByteBuffer.wrap(bytes);
		@SuppressWarnings("unchecked")
		T result = (T) BinaryHelper.fromByte(bytes[0]).unmarshall(buffer);
		return result;
	}
}
